<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage core
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

namespace mg;

/**
 * A mgBootstrapGuard guards the bootstrap process for deployment in production environments.
 * The availability of the bootstrap guard enables deployment to be a secure controlled event.
 *
 * @author Michiel Hakvoort
 * @version 1.0
 * @see mgCore
 */
interface BootstrapGuard {

    /**
     * Guard the bootstrap process.
     * If this method returns true, the deployment continues and the site operation will continue.
     * If this method returns false, further deployment is halted and the Mangrove core boot
     * procedure is halted.
     *
     * Any generated output or headers should be send in this method.
     *
     * @return boolean
     */
    public function guard();
}

/**
 * The mgHTTPAuth is a {@link mgBootstrapGuard bootstrap guard} which guards the bootstrap process through
 * http authentication. If a user is not authorized to bootstrap the process,
 * a 401 Unauthorized is displayed and the application will be terminated.
 * Otherwise, bootstrap will proceed normally.
 *
 * @author Michiel Hakvoort
 * @version 1.0
 * @see mgBootstrapGuard
 */
final class HTTPAuth implements BootstrapGuard {

    private $users = array();

    private $isAuthRequest = false;

    private $realm = null;
    private $message = null;

    private $response = null;

    /**
     * Create a new mgHTTPAuth guard within the given realm
     *
     * @param string $realm
     */
    public function __construct($realm) {
        if($realm === null) {
            throw new Exception("Realm not set");
        }

        $this->realm = $realm;
        if(isset($_SERVER['PHP_AUTH_DIGEST'])) {
            $this->response = $this->parseResponse($_SERVER['PHP_AUTH_DIGEST']);

            if($this->response) {
                $this->isAuthRequest = true;
            }
        }
    }

    /**
     * Parse the PHP_AUTH_DIGEST response
     *
     * @param string $response
     * @return array
     */
    private function parseResponse($response) {
        $i = 0;
        $result = array();

        $parseKey = true;
        $escape = false;
        $keyBuf = '';
        $valBuf = '';

        while($i != mb_strlen($response)) {
            $c = $response[$i++];
            if(!$escape && $c == '=') {
                $parseKey = false;
                continue;
            }
            if(!$escape && $c == ',') {
                $parseKey = true;
                $result[mb_strtolower(trim($keyBuf))] = trim($valBuf);
                $keyBuf = '';
                $valBuf = '';
                continue;
            }
            if($c == '"') {
                $escape = !$escape;
                continue;
            }

            if($parseKey) {
                $keyBuf .= $c;
            } else {
                $valBuf .= $c;
            }
        }
        $result[mb_strtolower(trim($keyBuf))] = trim($valBuf);
        $requiredFields = array('nonce', 'nc', 'cnonce', 'qop', 'username', 'uri', 'response');

        $isValidResponse = true;

        reset($requiredFields);

        $field = current($requiredFields);

        while($isValidResponse && $field) {
            $isValidResponse = isset($result[$field]);
            $field = next($requiredFields);
        }

        if(!$isValidResponse) {
            return false;
        }

        return $result;
    }

    /**
     * Authorize a user to the bootstrap the core.
     *
     * @access public
     * @param string $username
     * @param string $password
     */
    public function addUser($username, $password) {
        $this->users[$username] = $password;
    }

    /**
     * Remove authorization from a user to the bootstrap the core.
     *
     * @access public
     * @param string $username
     */
    public function removeUser($username) {
        unset($this->users[$username]);
    }

    /**
     * Set the displayed message for the authorization phase.
     *
     * @access public
     * @param string $message
     */
    public function setMessage($message) {
        $this->message = $message;
    }

    /**
     * {@inheritDoc}
     */
    public function guard() {

        if(!$this->isAuthRequest) {
            $this->showHeaders();
        } else {
            if(!isset($this->users[$this->response['username']])) {
                $this->showHeaders();
            }

            $a1 = md5($this->response['username'] . ':' . $this->realm . ':' . $this->users[$this->response['username']]);
            $a2 = md5($_SERVER['REQUEST_METHOD'].':'.$this->response['uri']);
            $check = md5($a1.':'.$this->response['nonce'].':'.$this->response['nc'].':'.$this->response['cnonce'].':'.$this->response['qop'].':'.$a2);
            	
            if($this->response['response'] != $check) {
                $this->showHeaders();
            }
        }
    }

    /**
     * Show the 401 unauthorized headers
     *
     * @access prive
     */
    private function showHeaders() {
        header('HTTP/1.1 401 Unauthorized');
        header('WWW-Authenticate: Digest realm="'.$this->realm.'",qop="auth",nonce="'.uniqid().'",opaque="'.md5($this->realm).'"');
        if($this->message !== null) {
            echo $this->message;
        }
    }
}
